#include <iostream>
#include <sstream>
#include <string>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>

#include <msg/PopupMessages.hpp>
#include <PopupDatabaseUI.hpp>
#include <PopupLoggerUI.hpp>
#include "PopupClient.hpp"

using namespace std;
using namespace Popup;
using namespace PopupUtils;

//=============================================================================
// Construction/Destruction
//=============================================================================

Client *Client::newClient(ClientUI *p_clientUI)
{
  return new ClientImpl(p_clientUI);
}
//---------------------------------------------------------

ClientImpl::ClientImpl(ClientUI *p_clientUI)
  : clientUI(p_clientUI), socket(0), state(CLIENT_STATE_DISCONNECTED),
    sslContext(CLIENT_CONTEXT), sessionID(-1),
    networker(0), server(""), port(0), threadCounter(0), messageCounter(0),
    sendFileCounter(0)
{
  PopupUtils::PopupThread::start(
      ClientImpl::startAttachmentSenderRoutine, this);
}
//---------------------------------------------------------

ClientImpl::~ClientImpl()
{
  disconnect();
}

//=============================================================================
// Connection/Disconnection
//=============================================================================

ConnectionEcode ClientImpl::connect(const string & p_server,
                                    unsigned short p_port,
                                    const string & p_login,
                                    const string & p_password,
                                    bool p_register)
{
  ConnectionEcode _rc = POPUP_CONNECTION_ERR_UNKNOWN;

  mutex.lock();
  if (state != CLIENT_STATE_DISCONNECTED)
  {
    mutex.unlock();
    error("State machine error: cannot connect");
  }
  else
  {
    state = CLIENT_STATE_CONNECTING;
    clientUI->onConnectionUpdate(false);

    _rc = doHandshake(p_server, p_port);

    if (_rc == POPUP_CONNECTION_SUCCESS) {
      _rc = authenticate(p_login, p_password, p_register);
    } else {
      state = CLIENT_STATE_DISCONNECTED;
      mutex.unlock();
    }

    if (_rc == POPUP_CONNECTION_SUCCESS) {
      state = CLIENT_STATE_CONNECTED;
      mutex.unlock();
      clientUI->onConnectionUpdate(true, user, sessionID);
      _rc = listen();
    } else {
      state = CLIENT_STATE_DISCONNECTED;
      mutex.unlock();
    }
  }

  return _rc;
}
//---------------------------------------------------------

bool ClientImpl::disconnect()
{
  bool _rc = false;
  mutex.lock();
  state = CLIENT_STATE_DISCONNECT;
  _rc = disconnectNoLock();
  mutex.unlock();
  if (_rc)
  {
    clientUI->onConnectionUpdate(false);
  }

  return _rc;
}
//---------------------------------------------------------

bool ClientImpl::disconnectNoLock()
{
  bool _rc = false;

  if (state != CLIENT_STATE_DISCONNECT)
  {
    error("state machine error: cannot disconnect.");
  }
  else
  {
    if (networker != 0) {
      delete networker;
      networker = 0;
    }
    if (socket != 0) {
      socket->close();
      delete socket;
      socket = 0;
    }
    state = CLIENT_STATE_DISCONNECTED;
    _rc = true;
  }
  return _rc;
}
//---------------------------------------------------------

ConnectionEcode ClientImpl::doHandshake(const string & p_server,
                                        unsigned short p_port)
{
  string _errMsg = "";
  ConnectionEcode _rc = POPUP_CONNECTION_SUCCESS;
  Message *_message = 0;
  bool _success = false;

  info("Connecting to server %s on port %d ...",
       p_server.c_str(), p_port);

  // Create client socket
  socket = PopupUtils::Socket::newClientSocket(p_server, p_port, &_errMsg);
  if (socket == 0)
  {
    _rc = POPUP_CONNECTION_ERR_SERVER_DOWN;
    error("Failed to connect : %s", _errMsg.c_str());
  }
  // Instantiate networker utility
  else
  {
    socket->enableKeepAlive(true, 10, this);
    networker = new MessageNetworker(socket, &sslContext);
  }

  if (_rc == POPUP_CONNECTION_SUCCESS) {

    // Send our RSA key to the server
    RsaPublicKeyMsg msg(&sslContext);
    trace("Sending RSA key.");
    _success = networker->send(&msg);

    // Wait for server RSA key in return
    if (_success)
    {
      if (networker->receive(&_message) &&
          (_message->header.type == POPUP_MSG_TYPE_RSA_PUBLIC_KEY))
      {
        RsaPublicKeyMsg remotekey(_message);
        _success = remotekey.isValid();
        if (_success) {
          trace("Received server RSA key.");
          sslContext.setRemoteRSApublicKey(remotekey.getKey(), remotekey.getSize());
        } else {
          error("Failed to receive server RSA key (the one received is not valid)");
        }
      }
      else {
        _success = false;
        error("Did not received server RSA key.");
      }
    }

    // At last send our blowfish key to the server
    if (_success)
    {
      trace("Sending Blowfish key.");
      BlowfishKeyMsg blowfish(&sslContext);
      _success = networker->send(&blowfish);
    }

    if (_success) {
      server = p_server;
      port   = p_port;
      trace("Handshake succeeded!");
    } else {
      error("Handshake failed!");
      _rc = POPUP_CONNECTION_ERR_HANDSHAKE_FAILED;
    }
  }

  return _rc;
}
//---------------------------------------------------------

ConnectionEcode ClientImpl::authenticate(const string & p_userid,
                                         const string & p_password,
                                         bool p_register)
{
  ConnectionEcode _rc = POPUP_CONNECTION_ERR_UNKNOWN;
  Message *_message = 0;

  // Send authentication message to server
  trace("Sending login request.");
  LoginMsg msg(p_userid, p_password, p_register);
  networker->send(&msg);

  // Wait server acknowledgment
  trace("Waiting server ACK...");
  if (networker->receive(&_message)) {

    ConnectionAck _ack(_message, clientUI->getResourceDir());

    if (_ack.isValid()) {
      _rc = _ack.getStatus();
      if (_rc == POPUP_CONNECTION_SUCCESS) {
        user = _ack.getUser();
        sessionID = _ack.getSessionID();
        trace("Connection success! Your session ID is 0x%08X.", sessionID);
        // Notify client that connection succeeded
        clientUI->onConnectionUpdate(true, user, sessionID);
      } else {
        error("Connection failed (rc=%d) : %s",
              _ack.getStatus(), _ack.getErrMessage().c_str());
      }
    } else {
      error("Received a bad acknowledgment message");
    }
  }

  if (_rc == POPUP_CONNECTION_SUCCESS) {

  }
  return _rc;
}

//=============================================================================
// RECEIVE FUNCTIONS IMPLEMENTATION
//=============================================================================

ConnectionEcode ClientImpl::listen()
{
  Message *_message = 0;
  bool _rc = true;

  mutex.lock();
  if (state != CLIENT_STATE_CONNECTED)
  {
    _rc = false;
  }
  mutex.unlock();

  while (_rc)
  {
    _rc = networker->receive(&_message);

    if (_rc)
    {
      switch (_message->header.type)
      {
        case POPUP_MSG_TYPE_USER_UPDATE: {
          UserUpdateMsg _updateMsg(_message, clientUI->getResourceDir());
          processUserUpdate(_updateMsg);
          break;
        }
        case POPUP_MSG_TYPE_MESSAGE: {
          UserMessageWrapper _userMsg(_message);
          processUserMessage(_userMsg);
          break;
        }
        case POPUP_MSG_TYPE_RATE_MESSAGE: {
          RateUserMessageWrapper _reactionMsg(_message);
          clientUI->onMessageReactionReceived(_reactionMsg);
          break;
        }
        case POPUP_MSG_TYPE_ATTACHMENT: {
          AttachmentMsg attachment(_message);
          processAttachment(attachment);
          break;
        }
        case POPUP_MSG_TYPE_CANVASS_SUBMIT: {
          CanvassSubmitMsg _canvass(_message);
          CanvassMap::iterator _it = canvassMap.find(_canvass.canvassID);
          if (_it == canvassMap.end()) {
            canvassMap[_canvass.canvassID] = new Canvass(_canvass);
          }
          clientUI->onOpinionRequested(_canvass);
          break;
        }
        case POPUP_MSG_TYPE_CANVASS_VOTE: {
          CanvassVoteMsg _vote(_message);
          clientUI->onVoteReceived(_vote);
          break;
        }
        case POPUP_MSG_TYPE_STATISTICS_UPDATE: {
          StatisticsUpdateMsg _statsMsg(_message);
          clientUI->onUserStatisticsUpdate(_statsMsg.getUserID(),
                                           _statsMsg.statistics());
          break;
        }
        case POPUP_MSG_TYPE_INVITATION: {
          ThreadInvitationMsg _invitation(_message);
          clientUI->onInvitationReceived(_invitation);
          break;
        }
        case POPUP_MSG_TYPE_KEEPALIVE_PROBE: {
          KeepAliveAck _ack;
          _rc = threadSafeMessageSend(_ack);
          break;
        }
        case POPUP_MSG_TYPE_KEEPALIVE_ACK: {
          // Good!! We're still connected!
          break;
        }
        default:
          trace("Received an unexpected message");
          break;
      }
      mutex.lock();
      if (state != CLIENT_STATE_CONNECTED) {
        _rc = false; // This will force listen loop exit!
      }
      mutex.unlock();
    }
  }
  disconnect();
  return POPUP_CONNECTION_ERR_LISTEN_LOOP_EXITED;
}
//---------------------------------------------------------

void ClientImpl::processUserUpdate(const UserUpdateMsg & p_updateMsg)
{
  User* _user = 0;
  UserMap::iterator _it;

  mutex.lock();

  _it = users.find(p_updateMsg.getID());
  // Update of an existing user
  if (_it != users.end()) {
    _user = _it->second;
    _user->update(p_updateMsg, p_updateMsg.getUpdateMask());
    clientUI->onUserUpdate(*_user, p_updateMsg.getUpdateMask(), false);
    if (_user->isAlive() == false) {
      delete _user;
      users.erase(_it);
    }
  }
  // New user
  else
  {
    _user = new User(p_updateMsg);
    users[_user->getID()] = _user;
    clientUI->onUserUpdate(*_user, p_updateMsg.getUpdateMask(), true);
  }

  mutex.unlock();
}
//---------------------------------------------------------

void ClientImpl::processUserMessage(const UserMessageWrapper & p_msg)
{
  //------ Message which attachments case
  if (p_msg.attachmentIDs.size() > 0) {
    for (set<AttachmentID>::iterator it = p_msg.attachmentIDs.begin();
        it != p_msg.attachmentIDs.end(); it++) {
    }
    recvMessageMap[p_msg.msgID] =
        new MessageWithAttachments(p_msg, p_msg.attachmentIDs);
  } else {
    clientUI->onMessageReceived(p_msg);
  }
}
//---------------------------------------------------------

void ClientImpl::processAttachment(const AttachmentMsg & p_msg)
{
  FileTransferExt *_recvinfo = 0;
  unsigned int _frameNo = p_msg.getFrameNo();
  bool _sameFileAlreadyExists = false;

  // Retrieve associated recv info
  RecvFileMap::iterator _it = recvFileMap.find(p_msg.getAttachmentID());

  if (_it != recvFileMap.end()) {
    _recvinfo = _it->second;
  }
  // Create if not found!
  else if (p_msg.getFrameNo() == 1)
  {
    PopupUtils::FileStream *_f = 0;

    // Compute name of the file to create
    string _filepath = clientUI->getTemporaryDir() +
        PopupUtils::Defs::FileSeparator + p_msg.getFilename();
    int _shiftVal = 1;
    while (PopupUtils::fexists(_filepath) && !_sameFileAlreadyExists) {

      // Compute existing file checksum
      unsigned long long _checksum = PopupUtils::checksum(_filepath);
      if (p_msg.getChecksum() == _checksum)
      {
        // File with the same name already exists!!
        _sameFileAlreadyExists = true;
        info("Dropping attachment %s (it already exists!)",
             _filepath.c_str());
      }
      else
      {
        _filepath = clientUI->getTemporaryDir() +
            PopupUtils::Defs::FileSeparator +
            PopupUtils::shiftFile(p_msg.getFilename(), _shiftVal++);
      }
    }

    // Open the file if file really needs to be received
    if (!_sameFileAlreadyExists)
    {
      _f = PopupUtils::openFileStream(_filepath.c_str(), "w");
      if (_f == 0) {
        error("Failed to open %s : %s",
              _filepath.c_str(), strerror(errno));
      }
    }

    if (_sameFileAlreadyExists || _f != 0)
    {
      info("Receiving %s", _filepath.c_str());
      _recvinfo = new FileTransferExt(p_msg.getParentMessageID(),
                                      p_msg.getAttachmentID(),
                                      _f, _filepath,
                                      p_msg.getNbFrames(),
                                      p_msg.getTotalSize());
      recvFileMap[p_msg.getAttachmentID()] = _recvinfo;
    }
  }
  // Strange!
  else {
    error("Unexpected attachment message");
  }

  if (_recvinfo != 0)
  {
    // Make sure frame number matches expected one
    if (_frameNo != _recvinfo->frameNo) {
      error("Ooops! Expected frame %lu, received %lu",
            _recvinfo->frameNo, _frameNo);
    }
    else
    {
      // Dump data to file in case descriptor is not null.
      // If descriptor is null, it means that file already exists
      // on host => ignore the file content data!!
      if (_recvinfo->file != 0)
      {
        p_msg.getRawData(_recvinfo->file);
      }

      // Update counters
      _recvinfo->nbTransferred += p_msg.getFrameSize();
      _recvinfo->nbRemaining   -= p_msg.getFrameSize();

      // Send notification to GUI if necessary
      if (_recvinfo->isNotificationRequired()) {
        clientUI->onFileTransferUpdate(*_recvinfo);
      }

      // Transfer not yet over...
      if (!_recvinfo->isComplete())
      {
        // ... set next expected frame
        _recvinfo->frameNo += 1;
      }
      // Or end up this transfer
      else
      {
        // Close file
        if (_recvinfo->file != 0)
        {
          PopupUtils::closeFileStream(_recvinfo->file);
        }

        // Retrieve parent message
        RecvMessageMap::iterator _it =
            recvMessageMap.find(p_msg.getParentMessageID());
        MessageWithAttachments *_msg = _it->second;

        _msg->setAttachmentReceived(p_msg.getAttachmentID(),
                                    _recvinfo->filepath);

        // In case all attachments have been successfully received,
        // client can be notified
        if (_msg->isComplete()) {
          clientUI->onMessageReceived(_msg->message);
          delete(_msg);
        }
      }
    }
  }
}

//=============================================================================
// SEND FUNCTIONS IMPLEMENTATION
//=============================================================================

bool ClientImpl::sendProbe()
{
  KeepAliveProbe _probe;
  return threadSafeMessageSend(_probe);
}
//---------------------------------------------------------

bool ClientImpl::notifyUserUpdate(const User *p_myself,
                                  unsigned short p_updateMask)
{
  UserUpdateMsg _updateMsg(*p_myself, p_updateMask);
  return threadSafeMessageSend(_updateMsg);
}
//---------------------------------------------------------

bool ClientImpl::sendMessage(UserMessage & p_message)
{
  bool _rc = true;
  std::vector<FileTransferExt*> _transfers;

  // Set thread ID
  if (p_message.threadID == 0) {
    threadCounter++;
    p_message.threadID = (((ThreadID) sessionID) << 32) | threadCounter;
  }
  // Set sender ID
  p_message.senderID = user.getID();

  // Set message ID
  messageCounter++;
  p_message.msgID = (((MessageID) sessionID) << 32) | messageCounter;

  // Create message
  UserMessageWrapper _msg(p_message);

  // Push all attachment IDs
  if (p_message.files.size() > 0)
  {
    AttachmentList::iterator _it;
    for (_it = p_message.files.begin(); _it != p_message.files.end(); _it++) {

      // Try to open file first
      FileStream *_f = openFileStream(_it->c_str(), "r");
      if (_f == 0) {
        error("Ooops! I'm a crap!! I cannot open this file: %s", _it->c_str());
        _rc = false;
      }
      else
      {
        // Compute attachment ID according to the schema:
        // <bit63-bit32>: session ID
        // <bit31-bit00>: counter
        sendFileCounter++;
        AttachmentID _attachmentID =
            (((AttachmentID) sessionID) << 32) | sendFileCounter;

        // Push into the message the ID of the attachment
        _msg.attachmentIDs.insert(_attachmentID);

        // Allocate a new file transfer
        _transfers.push_back(
            new FileTransferExt(p_message.msgID, _attachmentID,
                                p_message.targets, _f, *_it));
      }
    }
  }

  // Send message only if every previous steps were successful
  if (_rc)
  {
    _rc = threadSafeMessageSend(_msg);
    if (_rc) {
      clientUI->onMessageSent(p_message);
    }
  }

  if (_rc)
  {
    // Update send queue
    if (_transfers.size() > 0) {
      std::vector<FileTransferExt*>::iterator _xfer;
      mutex.lock();
      for (_xfer = _transfers.begin(); _xfer != _transfers.end(); _xfer++) {
        sendFileQueue.push(*_xfer);
      }
      mutex.unlock();
    }
  }

  return _rc;
}
//---------------------------------------------------------

bool ClientImpl::sendMessageReaction(const RateUserMessage & p_reaction)
{
  RateUserMessageWrapper _msg(p_reaction);
  return threadSafeMessageSend(_msg);
}
//---------------------------------------------------------

bool ClientImpl::sendInvitation(const ThreadInvitation & p_invitation)
{
  ThreadInvitationMsg _invitationMsg(p_invitation);
  return threadSafeMessageSend(_invitationMsg);
}
//---------------------------------------------------------

bool ClientImpl::submitCanvass(Canvass & p_canvass)
{
  bool _rc = true;

  // Set thread ID
  if (p_canvass.canvassID == 0) {
    threadCounter++;
    p_canvass.canvassID = (((ThreadID) sessionID) << 32) | threadCounter;
  }
  // Set sender ID
  p_canvass.senderID = user.getID();

  // Create message
  CanvassSubmitMsg _msg(p_canvass);

  // Send message
  _rc = threadSafeMessageSend(_msg);

  // Store a copy of the canvass in the map
  if (_rc) {
    canvassMap[p_canvass.canvassID] = new Canvass(p_canvass);
  }

  return _rc;
}
//---------------------------------------------------------

bool ClientImpl::sendVote(const Vote & p_vote)
{
  bool _rc = true;

  // Search for this canvass in the map
  CanvassMap::iterator _it = canvassMap.find(p_vote.canvassID);
  if (_it == canvassMap.end()) {
    error("Cannot find canvass %llu", p_vote.canvassID);
    _rc = false;
  } else {
    // Add all other voters in copy
    Canvass *_canvass = _it->second;
    UserList _targets;
    _targets.insert(_canvass->senderID);
    UserList::iterator _itvoter;
    for (_itvoter = _canvass->targets.begin();
         _itvoter != _canvass->targets.end(); _itvoter++) {
      _targets.insert(*_itvoter);
    }

    CanvassVoteMsg _vote(p_vote, _targets);
    _vote.voterID = user.getID();
    _rc = threadSafeMessageSend(_vote);
  }

  return _rc;
}

//=============================================================================
// Attachment sending management
//=============================================================================

void *ClientImpl::startAttachmentSenderRoutine(void *p_args)
{
  ((ClientImpl*) p_args)->attachmentSenderRoutine();
  return 0;
}
//---------------------------------------------------------

void ClientImpl::attachmentSenderRoutine()
{
  bool _rc = true;
  FileTransferExt *_sendinfo = 0;

  while (true)
  {
    _rc = true;
    _sendinfo = 0;

    mutex.lock();

    // We are not connected, let's wait
    if (state != CLIENT_STATE_CONNECTED)
    {
      // Clear the queue if not empty
      if (sendFileQueue.size() > 0) {
        SendFileQueue _empty;
        std::swap(sendFileQueue, _empty);
      }
      // And wait
      mutex.unlock();
      PopupUtils::PopupThread::sleep(1000);
    }

    // We are connected. Is there something to send?
    else
    {
      if (sendFileQueue.size() > 0)
      {
        _sendinfo = sendFileQueue.front();
        sendFileQueue.pop();
      }
      mutex.unlock();
    }

    // Nothing to send... Let's sleep
    if (_sendinfo == 0)
    {
      PopupUtils::PopupThread::sleep(1000);
    }
    // Hey! Cool! There is something to send! Let's go!
    else
    {
      for (_sendinfo->frameNo = 1;
           _sendinfo->frameNo <= _sendinfo->totalNbFrames;
           _sendinfo->frameNo++)
      {
        AttachmentMsg _part(_sendinfo);
        _rc = threadSafeMessageSend(_part);
        if (_rc)
        {
          _sendinfo->nbRemaining   -= _part.getFrameSize();
          _sendinfo->nbTransferred += _part.getFrameSize();
          // Notify client that we're sending this file
          if (_sendinfo->isNotificationRequired()) {
            clientUI->onFileTransferUpdate(*_sendinfo);
          }
        }
        else
        {
          break; // End loop in case we failed to send message
        }
      }
      PopupUtils::closeFileStream(_sendinfo->file);
      delete _sendinfo;
    }
  }
}

//=============================================================================
// Utilities
//=============================================================================

bool ClientImpl::threadSafeMessageSend(AbstractMessage & p_message)
{
  bool _rc = true;

  mutex.lock();
  if (state == CLIENT_STATE_CONNECTED) {
    _rc = networker->send(&p_message);
    if (!_rc) {
      printf("Send failed\n"); fflush(stdout);
      state = CLIENT_STATE_DISCONNECT;
      disconnectNoLock();
      clientUI->onConnectionUpdate(false);
    }
  } else {
    _rc = false;
  }
  mutex.unlock();

  return _rc;
}
//---------------------------------------------------------

bool ClientImpl::MessageWithAttachments::isComplete() const
{
  return (attachmentIDs.size() == 0);
}
//---------------------------------------------------------

void ClientImpl::MessageWithAttachments::setAttachmentReceived(
    AttachmentID p_attachmentID, const string & p_filepath)
{
  attachmentIDs.erase(p_attachmentID);
  message.files.push_back(p_filepath);
}
